package owg.util.editor2d;

import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.AWTEventListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Stack;
import java.util.WeakHashMap;

import com.jogamp.opengl.GL;
import com.jogamp.opengl.GL2GL3;
import com.jogamp.opengl.GLContext;
import javax.swing.JOptionPane;

import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.jogamp.opengl.util.GLBuffers;

import owg.util.Asserter;
import owg.util.Calc;
import owg.util.Camera2D;
import owg.util.EntityBase;
import owg.util.InstanceCondition;
import owg.util.InstanceList;
import owg.util.awt.ActionSource;
import owg.util.awt.StatefulJFrame;
import owg.util.data.KryoInstance;
import owg.util.data.PField;
import owg.util.data.PFieldElement;
import owg.util.data.PFieldUtil;
import owg.util.data.Pair;
import owg.util.data.property.Property;
import owg.util.editor2d.entity.*;
import owg.util.editor2d.modes.*;
import owg.util.editor2d.modes.BoxSelect.BoxSelectMode;
import owg.util.editor2d.operations.*;
import owg.util.euclidian.MatrixStack;
import owg.util.euclidian.V2D;
import owg.util.euclidian.V2F;
import owg.util.opengl.BlendMode;
import owg.util.opengl.ColorF;
import owg.util.opengl.EGL;
import owg.util.opengl.EGL3;
import owg.util.opengl.LineBuffer;
import owg.util.opengl.TTFFontRenderer;
import owg.util.opengl.Tex;
import owg.util.opengl.TextureLib;
import owg.util.opengl.ColorF.ColorFMutable;

public class Editor2D extends EntityBase 
{
	public static final ColorF[] bColors = {new ColorF(0x6c6c6c, false), new ColorF(0x72abd8, false), new ColorF(0xe46011, false), new ColorF(0x14ff00, false)};
	
	public static final byte MASK_LOCATION 	= 0b00000001;
	public static final byte MASK_SCALE 	= 0b00000010;
	public static final byte MASK_ROTATION 	= 0b00000100;
	
	private TTFFontRenderer font = null;
	
	public interface GameStartListener
	{
		public void stateChanged(boolean isStarted);
	}
	/**Condition dictating that only the {@link Editor2D} will receive step events during edit mode.*/
	public static InstanceCondition STEP_ONLY_EDITOR = (o) -> {return (o instanceof Editor2D);};
	/**Condition dictating that everything will receive step events during edit mode.*/
	public static InstanceCondition STEP_EVERYTHING_ALWAYS = null;
	/**Condition dictating that only instances that do not implement {@link Placeable2D} will receive 
	 * step events during edit mode; i.e. any objects that cannot be placed in the editor will still receive their step events as normal.*/
	public static InstanceCondition STEP_NOT_PLACEABLE2D = (o) -> {return !(o instanceof Placeable2D);};
	
	/**The maximum number of backups to maintain for each map*/
	public static final int NUM_BACKUPS = 16;
	
	/**If the distance from an object to the cursor to a non-{@link Touchable2D} object is less than this value, then it may be "highlighted".
	 * @see #getHighlightedObject()*/
	public double SELECT_DISTANCE = 16;
	/**The pivot mode, determining how objects are rotated and scaled, as well as which object will show up in the parameter editor.*/
	public static enum Pivot { MEAN, MEDIAN, FIRST_SELECTED, LAST_SELECTED }
	/**The possible transformation axes for scaling and translation.*/
	public static enum Axes 
	{
		VIEW, X, Y,
		LOCAL_X, LOCAL_Y;
	}
	
	private Queue<Object> eventQueue;
	protected WeakHashMap<Placeable2D, Integer> indexCache = new WeakHashMap<Placeable2D, Integer>();
	protected LinkedHashSet<GameStartListener> startListeners = new LinkedHashSet<>();
	
	private final ArrayList<Pair<Placeable2D,ConstructorParams>> worldObjects;
	public final List<Pair<Placeable2D,ConstructorParams>> worldObjectsImmutable;
	public final LinkedHashSet<Integer> selectedIndices = new LinkedHashSet<Integer>();
	private Mode mode = null;
	public final ActionSource modeListeners = new ActionSource() {};
	private UndoList undoList;
	private Placeable2D highlightedObject;
	
	public final InstanceList instanceList;
	public final Camera2D camera;
	public final InstanceCondition stepCondition;
	
	private File currentFile;
	public final boolean canEdit;
	private boolean isEditingEnabled;
	private boolean initialized = false;
	private volatile boolean toggleEditMode = false;
	private V2D mmbDrag = null;
	
	private int gridTexture = 0;
	private int gridTextureWidth = 0;
	private int gridTextureHeight = 0;
	
	public final File workingDir;
	public final Editor2DSettings settings;
	public final String[] packages;
	
	
	public String originTexture = "selectableCircle";
	public String mouseTexture = "editorCursor";
	public String cornerTexture = "editorCorner";
	
	public final Component mainPanel;
	private Editor2DUI editorUI;
	
	private Editor2DEventHandler eventHandler;
	
	public final LineBuffer lineBuffer;
	public byte sixtystage;
	
	
	/**
	 * Create a new editor.
	 * <br/><br/>
	 * <i>Parameters may not be null unless explicitly stated.</i>
	 * @param list The instance list to be used by the editor. Note that the editor self is an {@link EntityBase}, and it adds itself to this list.
	 * <i>As such, it is not necessary for the application to call {@link #step()} or {@link #render()} explicity.</i>
	 * @param canvas The OpenGL canvas which is used for rendering. The editor will automatically register/unregister any necessary event handlers.
	 * @param workingDir The working directory for this editor instance. Users are encouraged to save their map files in this folder.
	 * Also, various settings files are stored here.
	 * @param startFile The map file to load at startup. This parameter may be null. If it is null, then the editor will look for a file called 
	 * "defaultMap.txt" in the working directory. If it exists, this file must contain a file name relative to the working directory.
	 * "defaultMap.txt" will be overwritten if a new map is loaded or if the map is saved to a new file, so this mechanism should probably not 
	 * be used during deployment. It is only a convenience feature during editing.
	 * @param startImmediately Whether to "start" the map immediately. If false, the editor will start in edit mode. If true, the editor will 
	 * start in play mode, and any {@link Startable2D} objects in the editor after loading the start file  will receive the start event.
	 * During deployment, this should be <b>true</b>. 
	 * @param stepCondition Determines which objects in the instance list should receive step events during editing. The application may use
	 * constants {@link #STEP_EVERYTHING_ALWAYS}, {@link #STEP_NOT_PLACEABLE2D}, {@link #STEP_ONLY_EDITOR}, or any custom implementation. 
	 * @param cam The camera object to be used by the editor.
	 * @param canEdit Whether to allow the user to enter edit mode by pressing Tab.
	 * @param packages A list of packages in which to search for objects implementing {@link Placeable2D}. The search will be recursive.
	 * The caller should avoid supplying multiple overlapping packages(for example, both "game" and "game.blocks").
	 */
	public Editor2D(InstanceList list, Component canvas, File workingDir,  File startFile, 
		boolean startImmediately, InstanceCondition stepCondition, Camera2D cam, boolean canEdit, String... packages) 
	{
		super(list, Integer.MIN_VALUE);
		this.canEdit = canEdit;
		if(workingDir == null)
			throw new IllegalArgumentException("workingDir cannot be null.");
		this.workingDir = workingDir;
		verifyWorkingDir();
		
		this.stepCondition = stepCondition;
		//If the caller did not specify a file, use the one from last session, null if none
		if(startFile == null)
			startFile = getDefaultMapName();		
		
		this.mainPanel = canvas;
		undoList = new UndoList();
		lineBuffer = new LineBuffer();
		settings = Editor2DSettings.open(new File(workingDir, "editorSettings/settings.txt").getAbsolutePath());
		eventQueue = new LinkedList<>();
		instanceList = list;
		camera = cam;
		this.packages = packages;
		worldObjects = new ArrayList<>();
		worldObjectsImmutable = Collections.unmodifiableList(worldObjects);
		isEditingEnabled = false;
		setVisible(false);
		openInitialFile(startFile);
		setEditingEnabled(!startImmediately); 
		initialized = true;
		
		Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener()
		{
			@Override
			public void eventDispatched(AWTEvent e)
			{
				if(!(e.getSource() instanceof Window) && e.getSource() != mainPanel && e.getSource() instanceof Component && ((Component)e.getSource()).getFocusTraversalKeysEnabled())
					return;
				if(e instanceof KeyEvent && e.getID() == KeyEvent.KEY_PRESSED && ((KeyEvent)e).getKeyCode() == KeyEvent.VK_TAB)
					toggleEditMode = true;
			}
		}, AWTEvent.KEY_EVENT_MASK);
	}
	
	private void openInitialFile(File startFile)
	{		
		currentFile = null;
		try {
			open(startFile);
		} catch (FileNotFoundException e)
		{
			System.out.println("The file \""+startFile+"\""+" does not exist in the working directory "+workingDir);
		} catch (Exception e)
		{
			System.err.println("An error occurred in opening the file "+startFile+" in "+workingDir+": ");
			e.printStackTrace();
		}
	}
	
	public void setEditingEnabled(boolean b) {
		if(b ^ isEditingEnabled || !initialized)
		{
			if(b)
			{
				//Need to reopen the map(except during initialization)
				if(initialized)
				{
					if(currentFile == null)
						clearMap();
					else
					{
						try
						{
							open(currentFile);
						}
						catch (IOException e)
						{
							JOptionPane.showMessageDialog(null, "Could not reopen the map file!\n"+e.getMessage(), 
								"Failed to open map!", JOptionPane.ERROR_MESSAGE);
							e.printStackTrace();
							clearMap();
						}
					}
				}
				isEditingEnabled = true;
				if(eventHandler == null)
					eventHandler = new Editor2DEventHandler(this);
				if(editorUI == null)
					editorUI = new Editor2DUI(this);
				editorUI.setEnabled(true);
				instanceList.setStepCondition(stepCondition);
				setVisible(true);
				for(GameStartListener l : startListeners)
					l.stateChanged(false);
			}
			else
			{
				//Need to save the map(except during initalization
				boolean r;
				if(initialized)
					r = editorUI.save(false);
				else
					r = true;
				
				if(!r)
				{
					JOptionPane.showMessageDialog(null, "Cannot start the game until the file has been saved.", "File was not saved", JOptionPane.INFORMATION_MESSAGE);
					editorUI.windowCloseCancelled();
				}
				else
				{
					if (initialized)
					{
						instanceList.setStepCondition(null);
						editorUI.setEnabled(false);
						eventHandler.destroy();
						eventHandler = null;
					}
					isEditingEnabled = false;
					setVisible(false);
					
					for(Pair<Placeable2D, ConstructorParams> p : worldObjects)
					{
						if(p.a instanceof Startable2D)
							((Startable2D) p.a).start();
					}
				}
				for(GameStartListener l : startListeners)
					l.stateChanged(true);
			}
		}
	}
	public boolean isEditingEnabled()
	{
		return isEditingEnabled;
	}
	public Editor2DEventHandler eventHandler()
	{
		return eventHandler;
	}
	public File currentFile()
	{
		return currentFile;
	}
	private void verifyWorkingDir()
	{
		//Verify/create working dir dir
		if(!workingDir.exists())
		{
			if (!workingDir.mkdirs())
				throw new RuntimeException("The editor was unable to create the working directory "+workingDir);
		}
		else if(!workingDir.isDirectory())
			throw new RuntimeException("The working directory "+workingDir+" exists, but it is not a directory!");
	}
	
	@Override
	public void destroy() 
	{
		settings.save(this);
		if(eventHandler != null)
			eventHandler.destroy();
		eventHandler = null;
	}
	
	@Override
	public void beginStep() 
	{}
	
	@Override
	public void step() 
	{
		sixtystage = (byte) Calc.cyclic(sixtystage+1, 60);
		
		if(isEditingEnabled)
		{
			//Find which object to highlight
			if(mode == null || mode.shouldUpdateHighlight())
				chooseHighlightedObject();
			
			Object next;
			while((next = dequeue()) != null)
			{
				if(next instanceof MouseWheelEvent)//Note: this is an instanceof MouseEvent, don't move it below
				{
					MouseWheelEvent e = (MouseWheelEvent) next;
					if(mode != null)
						mode.wheel(e);
				}
				else if(next instanceof MouseEvent)
				{
					MouseEvent e = (MouseEvent) next;
					if(e.getID() == MouseEvent.MOUSE_PRESSED && e.getButton() == MouseEvent.BUTTON2)
					{
						mmbDrag = eventHandler.getMousePosition();
					}
					else if(e.getID() == MouseEvent.MOUSE_RELEASED && e.getButton() == MouseEvent.BUTTON2)
					{
						mmbDrag = null;
					}
					else if(e.getID() == MouseEvent.MOUSE_DRAGGED && mmbDrag != null)
					{
						V2D pos = eventHandler.getMousePosition();
						V2D offset = pos.clone().sub(mmbDrag);
						camera.pos.sub(offset);
						mmbDrag.set(pos).sub(offset);
					}
						
					if(mode == null)
					{
						if(e.getID() == MouseEvent.MOUSE_PRESSED && e.getButton() == MouseEvent.BUTTON1)
							setMode(new Paint(this));
						else if(e.getID() == MouseEvent.MOUSE_PRESSED && e.getButton() == MouseEvent.BUTTON3)
						{
							if(highlightedObject != null)
							{
								if(e.isShiftDown())
								{
									storeState();
									boolean remove = isSelected(highlightedObject); 
									if(remove)
										Asserter.check(selectedIndices.remove(new Integer(indexOf(highlightedObject))));
									else
										selectedIndices.add(indexOf(highlightedObject));
									execute(Operation.NO_OPERATION, Operation.NO_OPERATION, true, 
										(remove?"Removed ":"Added")+highlightedObject+" to selection", false, false);
								}
								else if(selectedIndices.size() != 1 || !isSelected(highlightedObject))//Nop if nothing will change
								{
									storeState();
									selectedIndices.clear();
									selectedIndices.add(indexOf(highlightedObject));
									execute(Operation.NO_OPERATION, Operation.NO_OPERATION, true, "Selected "+highlightedObject, false, false);
								}
							}
							else if (!e.isShiftDown() && !selectedIndices.isEmpty())//Deselect all if anything is selected
							{
								storeState();
								selectedIndices.clear();
								execute(Operation.NO_OPERATION, Operation.NO_OPERATION, true, "Cleared selection", false, false);
							}
						}
					}
					if(mode != null)
						mode.mouse(e);
				}
				else if(next instanceof KeyEvent)
				{
					KeyEvent e = (KeyEvent) next;
					
					if(mode != null)
						mode.key(e);
					else
					{
						if(e.getID() == KeyEvent.KEY_PRESSED)
						{
							if(e.getKeyCode() == KeyEvent.VK_SPACE && e.getModifiers() == KeyEvent.SHIFT_MASK)
								settings.createPaint.set(!settings.createPaint.get());
							else if(e.getKeyCode() == KeyEvent.VK_X && e.getModifiers() == KeyEvent.SHIFT_MASK)
								settings.destroyPaint.set(!settings.destroyPaint.get());
							else if(e.getKeyCode() == KeyEvent.VK_B && e.getModifiers() == 0)
								setMode(new BoxSelect(this, BoxSelectMode.BOX));
							else if((e.getKeyCode() == KeyEvent.VK_DELETE || e.getKeyCode() == KeyEvent.VK_X) && e.getModifiers() == 0)
								deleteSelected();
							else if(e.getKeyCode() == KeyEvent.VK_G && e.getModifiers() == KeyEvent.SHIFT_MASK)
								settings.snapEnabled.set(!settings.snapEnabled.get());
							else if(e.getKeyCode() == KeyEvent.VK_G && e.getModifiers() == KeyEvent.ALT_MASK)
								reset(MASK_LOCATION);
							else if(e.getKeyCode() == KeyEvent.VK_G)
								setMode(new Translate(this, Axes.VIEW, false));
							else if(e.getKeyCode() == KeyEvent.VK_R && e.getModifiers() == KeyEvent.ALT_MASK)
								reset(MASK_ROTATION);
							else if(e.getKeyCode() == KeyEvent.VK_R)
								setMode(new Rotate(this, false));
							else if(e.getKeyCode() == KeyEvent.VK_S && e.getModifiers() == KeyEvent.SHIFT_MASK)
								settings.snapScaleEnabled.set(!settings.snapScaleEnabled.get());
							else if(e.getKeyCode() == KeyEvent.VK_S && e.getModifiers() == KeyEvent.ALT_MASK)
								reset(MASK_SCALE);
							else if(e.getKeyCode() == KeyEvent.VK_S)
								setMode(new Scale(this, Scale.ScaleMode.SCALE_AND_MOVE, Axes.VIEW));
							else if(e.getKeyCode() == KeyEvent.VK_L)
								setMode(new Link(this));
							else if(e.getKeyCode() == KeyEvent.VK_COMMA)
							{
								Pivot[] vals = Pivot.values();
								int ind = Calc.cyclic(settings.pivot.get().ordinal()+(e.isShiftDown()?-1:1),vals.length);
								settings.pivot.set(vals[ind]);
							}
							else if(e.getKeyCode() == KeyEvent.VK_Z && e.isControlDown())
							{
								if(e.isAltDown() || e.isShiftDown())
									redo();
								else
									undo();
							}
							else if(e.getKeyCode() == KeyEvent.VK_Y && e.getModifiers() == KeyEvent.CTRL_MASK)
								redo();
							else if(e.getKeyCode() == KeyEvent.VK_S && e.isControlDown() && !e.isShiftDown())
								editorUI.save(e.isAltDown());
							else if(e.getKeyCode() == KeyEvent.VK_O && e.getModifiers() == KeyEvent.CTRL_MASK)
								editorUI.open();
							else if(e.getKeyCode() == KeyEvent.VK_N && e.getModifiers() == KeyEvent.CTRL_MASK)
								editorUI.clearMap();
							else if(e.getKeyCode() == KeyEvent.VK_A)
							{
								if(selectedIndices.isEmpty())
									selectAll(!e.isControlDown());
								else
									selectNone();
							}
							else if(e.getKeyCode() == KeyEvent.VK_ESCAPE && isEditingEnabled)
								toggleEditMode = true;
						}
					}
				}
				else if(next instanceof Runnable)
				{
					editorUI.editor.terminateMode(true);
					((Runnable) next).run();
				}
				else
					System.err.println("Warning: unknown event: "+next);
			}
		}
		//Make sure to toggle editor mode after all other events are processed.
		if(editorUI.getWindowState() == Editor2DUI.WINDOW_CLOSING && isEditingEnabled)
			setEditingEnabled(false);
		else if(toggleEditMode && (isEditingEnabled || canEdit))
			setEditingEnabled(!isEditingEnabled);
		toggleEditMode = false;
	}
	/**
	 * Reset the selected objects' parameters
	 * @param mask An OR'ed combination of {@link #MASK_LOCATION}, {@link #MASK_ROTATION} and {@link #MASK_SCALE}
	 */
	void reset(byte mask)
	{
		StringBuilder sb = new StringBuilder("Reset ");
		Integer[] selected = new Integer[selectedIndices.size()];
		selectedIndices.toArray(selected);
		Operation inv = new Transformation(this, selected);
		boolean first = true;
		if((mask&MASK_LOCATION)!=0)
		{
			for(int index : selected)
				worldObjects.get(index).a.setLocation(0, 0);
			sb.append("location");
			first = false;
		}
		if((mask&MASK_ROTATION)!=0)
		{
			for(int index : selected)
			{
				if(worldObjects.get(index).a instanceof Rotateable2D)
					((Rotateable2D)worldObjects.get(index).a).setAngle(0);
			}
			if(!first)
				sb.append(", ");
			sb.append("rotation");
			first = false;
		}
		if((mask&MASK_SCALE)!=0)
		{
			final float sc;
			if(settings.snapScaleEnabled.get())
			{
				if(settings.snapScaleSize.get() == 0)
					sc = 1.0f;
				else
					sc = settings.snapScaleSize.get();
			}
			else
				sc = 1.0f;
			for(int index : selected)
			{
				if(worldObjects.get(index).a instanceof Scaleable2D)
					((Scaleable2D)worldObjects.get(index).a).setScale(sc, sc);
			}
			if(!first)
				sb.append(", ");
			sb.append("scale");
			first = false;
		}
		Operation op = new Transformation(this, selected);
		execute(op, inv, false, sb.toString(), true, false);
	}

	void selectNone()
	{
		storeState();
		selectedIndices.clear();
		execute(Operation.NO_OPERATION, Operation.NO_OPERATION, true,
			"Cleared selection", false, false);	
	}
	void selectAll(boolean onScreenOnly)
	{
		storeState();
		V2D v = new V2D();
		if(onScreenOnly)
			selectedIndices.clear();
		for(int i = 0; i< worldObjects.size(); i++)
		{
			Placeable2D p = worldObjects.get(i).a;
			v.set(p.getX(), p.getY());
			if(camera.isOnScreen(v) || !onScreenOnly)
				selectedIndices.add(i);
		}
		execute(Operation.NO_OPERATION, Operation.NO_OPERATION, true,
			"Selected all"+(onScreenOnly?" on screen":""), false, false);
	}
	
	void setMode(Mode m)
	{
		assert m != null : "This method should not be used to clear mode. Use terminateMode or modeFinished as appropriate.";
		mode = m;
		editorUI.setParamsLocked(true);
		modeListeners.fireEvents(0);
	}
	
	void deleteSelected() {
		if(mode != null)
			terminateMode(true);
		if(!selectedIndices.isEmpty())
		{
			Integer[] selected = new Integer[selectedIndices.size()];
			selectedIndices.toArray(selected);
			Destroyer destroyer = new Destroyer(selected);
			Reviver reviver = new Reviver(destroyer);
			execute(destroyer, reviver, true, "Destroyed selected objects", true, false);
		}
	}
	
	private void chooseHighlightedObject()
	{
		double minDist = Double.MAX_VALUE;
		V2D mouse = eventHandler.getMousePosition();
		if(settings.snapSelect.get())
			eventHandler.correctToGrid(mouse);
		V2D v = new V2D();
		//Find the closest object, and, if there are multiple objects at that exact distance, put them into a queue.
		Queue<Placeable2D> closest = new LinkedList<Placeable2D>();
		for(Pair<Placeable2D, ConstructorParams> p : worldObjects)
		{
			v.set(p.a.getX(), p.a.getY());
			if(camera.isOnScreen(v))
			{
				double d;
				if(p.a instanceof Touchable2D && !(mode instanceof BoxSelect))
					d = ((Touchable2D) p.a).touches(mouse.x(), mouse.y())?1:Double.MAX_VALUE;
				else
					d = v.distance(mouse);
				if(d < minDist) 
				{
					minDist = d;
					closest.clear();
				}
				if(d == minDist)
				{
					closest.add(p.a);
				}
			}
		}
		//If the closest object is within the selection distance
		if(minDist < SELECT_DISTANCE)
		{
			assert !closest.isEmpty();
			if(closest.size() == 1)//If there is a single best candidate, then use that
				highlightedObject = closest.poll();
			else//If there are multiple candidates, then use the one with the next ID compared to the current selection, in-order and wrapping around.
			{
				Placeable2D[] candidates = new Placeable2D[closest.size()];
				closest.toArray(candidates);
				closest = null;
				long highestSelectedID = -1;
				for(Placeable2D p : candidates)
					if(isSelected(p) && p.getID() > highestSelectedID)
						highestSelectedID = p.getID();
				
				highlightedObject = candidates[0];
				if(highestSelectedID != -1)
				{
					/* Choose the ID with the shortest wrap-around distance from the highest selected ID:*/
					for(int i = 1; i<candidates.length; i++)
					{
						Placeable2D c = candidates[i];
						if (highlightedObject.getID() > highestSelectedID)
						{
							//We are already above the threshold, only count down
							if(c.getID() < highlightedObject.getID())
								highlightedObject = c;
						}
						else
						{
							//We are not above the threshold. Either breach the threshold or count down
							if(c.getID() > highestSelectedID || c.getID() < highlightedObject.getID())
								highlightedObject = c;
						}	
					}
				}
			}
		}
		else
			highlightedObject = null;
	}
	
	@Override
	public void endStep() 
	{}
	
	public void render() 
	{
		EGL<?> egl = EGL.getEGL(GLContext.getCurrentGL());
		MatrixStack m = egl.modelviewMatrix();
		
		ColorFMutable color = ColorF.WHITE.getMutableCopy();
		color.setAlpha(0.75f);
		
		Tex circle = TextureLib.tex("selectableCircle");
		
		float[] linkColor = bColors[3].getFloat();
		V2D v = new V2D();
		HashSet<Pair<Object, Object>> handledConnections = new HashSet<>();
		for (int i = 0; i < worldObjects.size(); i++)
		{
			Placeable2D p = worldObjects.get(i).a;
			v.set(p.getX(), p.getY());
			if(camera.isOnScreen(v) || isSelected(p) || p == highlightedObject)
			{
				int subImage = (isSelected(p)?2:0) + (getHighlightedObject()==p?1:0);
				if((!(p instanceof Touchable2D) || mode instanceof BoxSelect))
				{
					egl.setColor(color);
					egl.enableTexture2D(circle, subImage);
					egl.unitSquare.render(v, 8);
				}
				if(subImage > 0)
				{
					egl.disableTexture2D();
					GL2GL3 gl = (GL2GL3)egl.getGL();
					gl.glPolygonMode(GL2GL3.GL_FRONT_AND_BACK, GL2GL3.GL_LINE);
					gl.glLineWidth(3);
					egl.setColor(ColorF.BLACK);
					p.render(true);
					gl.glLineWidth(1);
					egl.setColor(bColors[subImage]);
					p.render(true);
					gl.glPolygonMode(GL2GL3.GL_FRONT_AND_BACK, GL2GL3.GL_FILL);
					egl.setStippleEnabled(false);
				}
			}
			PField<?>[] pFields = PFieldUtil.getPFields(p);
			for(PField<?> f : pFields)
			{
				if(Placeable2D.class.isAssignableFrom(f.getType()))
				{
					for(PFieldElement o : f)
					{
						Placeable2D p2 = (Placeable2D) o;
						byte func = Link.getFunc(f, p2);
						if(func != 0)
						{
							Pair<Object, Object> connection = new Pair<>(p, o);
							if(!handledConnections.contains(connection))
							{
								putLink(p.getX(), p.getY(), p2.getX(), p2.getY(), func, linkColor);
								handledConnections.add(connection);
							}
						}
					}
				}
			}
		}
		
		
		
		egl.setColor(ColorF.WHITE);
		V2D mouse = eventHandler.getMousePosition();
		V2F mouse2F = mouse.asV2F();
		eventHandler.correctToGrid(mouse);
		V2F mouseSnap = mouse.asV2F();
		
		egl.setBlendMode(BlendMode.INVERT);
		if(mode == null || mode.shouldUpdateHighlight())
		{
			if(settings.snapEnabled.get() && settings.snapSelect.get())
			{
				m.push();
				egl.enableTexture2D(TextureLib.tex(cornerTexture), 0);
				egl.unitSquare.render(mouseSnap, 4);
				m.pop();
			}
		}
		
		if(settings.snapEnabled.get() && settings.snapVisible.get())
		{
			int w = settings.snapWidth.get();
			int h = settings.snapHeight.get();
			if(w != gridTextureWidth || h != gridTextureHeight)
			{
				GL gl = egl.getGL();
				if(gridTexture != 0)
					gl.glDeleteTextures(1, new int[]{gridTexture}, 0);
				if(w > 1 && h > 1)
				{
					int[] t = new int[1];
					gl.glGenTextures(1, t, 0);
					gridTexture = t[0];
					
					gl.glBindTexture(GL.GL_TEXTURE_2D, gridTexture);
					gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_REPEAT);
					gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_REPEAT);
					gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST);
					gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST);
					byte[] gt = new byte[w*h];
					Arrays.fill(gt, 0, w, (byte)0xFF);
					for(int i = w; i<gt.length; i += w)
					{
						gt[i] = (byte)0xFF;
					}
					int minSize = GLBuffers.sizeof(gl, t, egl.alphaFormat(), GL.GL_UNSIGNED_BYTE, w, h, 1, false);
					ByteBuffer pixels;
					if(gt.length < minSize)
						pixels = (ByteBuffer) ByteBuffer.allocate(minSize).put(gt).rewind();
					else
						pixels = ByteBuffer.wrap(gt);
					gl.glTexImage2D(GL.GL_TEXTURE_2D, 0, egl.alphaFormat(), w, h, 0, egl.alphaFormat(), GL.GL_UNSIGNED_BYTE, pixels);
				}
				gridTextureWidth = w;
				gridTextureHeight = h;
			}

			if(gridTexture != 0)
			{
				egl.enableTexture2D(gridTexture, w, h);
				if(egl instanceof EGL3)
				{
					egl.setColor(ColorF.GRAY);
					MatrixStack t = egl.textureMatrix();
					EGL3 egl3 = (EGL3) egl;
					egl3.useProgram(egl3.alphaTextureShaderProgram);
					V2D upperLeft = eventHandler.correctToGrid(camera.pos.clone(), true);
					float tw = (float)(camera.size.x()+settings.snapWidth.get());
					float th = (float)(camera.size.y()+settings.snapHeight.get());
					m.push();
					m.translatef((float)upperLeft.x(), (float)upperLeft.y(), 0);
					m.scalef(tw, th, 1);
					t.push();
					t.scalef(tw/w, th/h, 1);
					egl.nwSquare.render();
					t.pop();
					m.pop();
					egl3.useProgram(egl3.defaultShaderProgram);
				}
			}
		}
		
		egl.setBlendMode(BlendMode.NORMAL);
		egl.setColor(ColorF.WHITE);
		
		egl.disableTexture2D();
		
		if(mode != null)
			mode.render(egl);
		
		egl.setBlendMode(BlendMode.NORMAL);
		egl.setColor(ColorF.WHITE);
		lineBuffer.render(egl);
		lineBuffer.clear();
		
		m.push();
		egl.enableTexture2D(TextureLib.tex(mouseTexture), 0);
		egl.nwSquare.render(mouse2F, 15);
		m.pop();
		
		egl.disableTexture2D();
	}
	
	public synchronized void enqueue(Object o) 
	{
		eventQueue.offer(o);
	}
	private synchronized Object dequeue()
	{
		return eventQueue.poll();
	}
	/**Returns the first or last object in the selection, depending on the pivot mode.
	 * If the pivot mode is {@link Pivot#FIRST_SELECTED}, then this will return the first object that was selected in the current selection.
	 * Otherwise, this returns the last object that was selected.
	 * If no object is selected, then this returns null.*/
	public Placeable2D getPivotObject() 
	{
		if(selectedIndices.isEmpty())
			return null;
		if(settings.pivot.get() == Pivot.FIRST_SELECTED)
			return worldObjects.get(extract(selectedIndices, 0)).a;
		else
			return worldObjects.get(extract(selectedIndices, selectedIndices.size()-1)).a;
	}
	
	/**Sets the currently selected indices. If writeUndo is true, then this will be recorded as a separate, single operation in the undo list.*/
	public void setSelectedIndices(LinkedHashSet<Integer> newIndices, boolean writeUndo)
	{
		if(writeUndo)
			storeState();
		selectedIndices.clear();
		if(newIndices != null)
			selectedIndices.addAll(newIndices);
		if(writeUndo)
			execute(Operation.NO_OPERATION, Operation.NO_OPERATION, true, "Select objects in list", false, false);
	}
	
	/**
	 * Execute the given operation, creating an undo record.
	 * @param op The operation to execute
	 * @param inverse The inverse operation to be executed upon a call to undo()
	 * @param execute Whether to execute the operation. Should be false only if the operation has effectively taken place already.
	 * @param autoStoreState Whether to automatically store the current state before executing the operation.
	 * If false, the caller must be sure that this has been called first.
	 * @param appendLastOperation Whether this operation and the previous operation are part of an atomic edit.
	 * Operations that are chained together like this will always be undone/redone as one.
	 * @return Whether the operation was completed.
	 */
	public boolean execute(Operation op, Operation inverse, boolean execute, String message, boolean autoStoreState, boolean appendLastOperation)
	{
		StateInfo oldState = null;
		try
		{
			if(autoStoreState)
				storeState();
			assert undoList.stateList[undoList.position] != null : 
				"The current selection state must be stored before executing an operation! AutoStore was: "+autoStoreState;
			oldState = undoList.stateList[undoList.position];
			
			undoList.strings[undoList.position] = message;
			undoList.undoList[undoList.position] = inverse;
			undoList.redoList[undoList.position] = op;
			if(appendLastOperation && undoList.position>0)
			{
				undoList.undoCounts[undoList.position] = undoList.undoCounts[undoList.position-1];
				undoList.undoCounts[undoList.position].set(undoList.undoCounts[undoList.position].get()+1);
			}
			else
				undoList.undoCounts[undoList.position] = new Property<Integer>(1);
			if(execute)
				op.apply(this);
			undoList.position++;
			
			//Push old records off the list
			if(undoList.position >= UndoList.MAX_UNDO_LIST_SIZE)
			{
				for(int i = 0; i < UndoList.MAX_UNDO_LIST_SIZE-1; i++)
				{
					undoList.undoList[i] = undoList.undoList[i+1];
					undoList.redoList[i] = undoList.redoList[i+1];
					undoList.stateList[i] = undoList.stateList[i+1];
					undoList.strings[i] = undoList.strings[i+1];
					undoList.undoCounts[i] = undoList.undoCounts[i+1];
				}
				undoList.position--;
			}
			else
			{	
				//Ensure that invalidated future records cannot be applied
				undoList.undoList[undoList.position] = null; 
				undoList.redoList[undoList.position] = null;
				undoList.strings[undoList.position] = null;
				undoList.undoCounts[undoList.position] = null;
				if(undoList.position < UndoList.MAX_UNDO_LIST_SIZE-1)
					undoList.stateList[undoList.position+1] = null;
			}
			editorUI.message(message);
			return true;
		}
		catch (OperationFailedException e)
		{
			System.err.println("Could not complete the operation!");
			editorUI.message("Operation failed! "+e.getMessage());
			e.printStackTrace();
			//Stuff got fraked up, try to fix it
			undoList.undoList[undoList.position] = null;
			undoList.redoList[undoList.position] = null;
			undoList.strings[undoList.position] = null;
			applyState();
			return false;
		}
		finally
		{
			
			//Note: This fails if the selected indices remain the same, but the objects at those indices change.
			//(In reality, this never happens.)
			if(oldState == null || !oldState.compareSet(selectedIndices))
			{
				editorUI.selectionChanged();
			}
			editorUI.operationCompleted();
		}
	}
	
	void undo()
	{
		if(undoList.position > 0 && undoList.undoCounts[undoList.position-1] != null)
			undo(undoList.undoCounts[undoList.position-1].get());
	}
	private void undo(int maxSteps)
	{
		//StateInfo oldState = stateList[undoList.position]; There might not be a defined state when undoing.
		StateInfo oldState = new StateInfo(selectedIndices);//This is harmless.
		try
		{
			terminateMode(true);
			if(undoList.position <= 0)
				return;
			storeState();
			undoList.position--;
			undoList.undoList[undoList.position].apply(this);
			applyState();
			editorUI.message("Undo: "+undoList.strings[undoList.position]);
			if(maxSteps > 1)
				undo(maxSteps-1);
		}
		catch (OperationFailedException e)
		{
			System.err.println("Could not undo!");
			editorUI.message("Undo failed! "+e.getMessage());
			e.printStackTrace();
			undoList.position++;
			applyState();
		}
		finally
		{
			if(oldState == null || !oldState.compareSet(selectedIndices))
				editorUI.selectionChanged();
			editorUI.operationCompleted();
		}
	}
	
	void redo()
	{
		if(undoList.position < UndoList.MAX_UNDO_LIST_SIZE && undoList.undoCounts[undoList.position] != null)
			redo(undoList.undoCounts[undoList.position].get());
	}
	private void redo(int maxSteps)
	{
		StateInfo oldState = undoList.stateList[undoList.position];
		try
		{
			terminateMode(true);
			if(undoList.position >= UndoList.MAX_UNDO_LIST_SIZE-1 || 
				undoList.undoList[undoList.position] == null || 
				undoList.redoList[undoList.position] == null ||
				undoList.stateList[undoList.position+1] == null)
				return;
			storeState();
			undoList.redoList[undoList.position].apply(this);
			editorUI.message("Redo: "+undoList.strings[undoList.position]);
			undoList.position++;
			applyState();
			if(maxSteps > 1)
				redo(maxSteps-1);
		}
		catch (OperationFailedException e)
		{
			System.err.println("Could not redo!");
			editorUI.message("Redo failed! "+e.getMessage());
			e.printStackTrace();
			applyState();
		}
		finally
		{
			if(oldState == null || !oldState.compareSet(selectedIndices))
				editorUI.selectionChanged();
			editorUI.operationCompleted();
		}
	}
	
	public void storeState()
	{
		undoList.stateList[undoList.position] = new StateInfo(selectedIndices);
	}
	
	public void applyState()
	{
		undoList.stateList[undoList.position].apply(this);
	}
	/**Called when an object has been instantiated by the user.
	 * Will end all current transformations and clear the selection.
	 * The indicated object will be added to the list of instances.
	 * @param placement if true, the transformation will be set to INITIAL_PLACEMENT. constructorInvoker must be defined.
	 * @param setSelection if true, the list of selected objects is cleared and this object is added.
	 * @param constructorInvoker if set, the WorldEditor's constructorOp is set to this value.
	 * @param createAtIndex The index where we want the object inserted into the list of world objects. -1 if irrelevant.
	 * @return the index where the object is added.*/
	public int objectCreated(Pair<Placeable2D,ConstructorParams> o, boolean clearSelection, boolean appendSelection, int createAtIndex)
	{
		assert ( !o.a.isDestroyed());
		if(createAtIndex == -1)
			createAtIndex = worldObjects.size();
		if(createAtIndex != worldObjects.size())
		{
			//Need to shuffle selectedIndices because the object is not inserted at the end
			LinkedHashSet<Integer> old = new LinkedHashSet<Integer>(selectedIndices);
			selectedIndices.clear();
			for(Integer j : old)
			{
				if(j >= createAtIndex)
					selectedIndices.add(j+1);
				else
					selectedIndices.add(j);
			}
		}
		worldObjects.add(createAtIndex, o);
		if(clearSelection)
			selectedIndices.clear();
		if(appendSelection)
		{
			final int ind = indexOf(o);
			assert ( ind != -1 && (createAtIndex == -1 || ind == createAtIndex) );
			selectedIndices.add(ind);
		}
		return createAtIndex;
	}
	/**Called from an operation when an object creation is undone. 
	 * This is different from normal destruction in that the object can never be revived.
	 * After this is done, it will be as if the object never existed at all.
	 * If the object creation is redone, a new object will be created.*/
	public void objectUncreated(Pair<Placeable2D,ConstructorParams> p)
	{
		int index = indexOf(p);
		if(index == -1)
			System.err.println("Warning: Attempting to remove unknown object "+p.a);
		worldObjects.remove(index);
		selectedIndices.remove(new Integer(index));
	}
	
	/**Called from an Operation when the indicated objects have already been destroyed.
	 * Returns the indices where these objects must be added if they are to be revived.
	 * The objects must then be added in reverse order.*/
	public int[] objectsDestroyed(ArrayList<Pair<Placeable2D, ConstructorParams>> destroyedObjects)
	{
		int[] indices = new int[destroyedObjects.size()];
		for(int j = 0; j<destroyedObjects.size(); j++)
		{
			assert ( destroyedObjects.get(j).a.isDestroyed()) : destroyedObjects.get(j).a+" hasn't been destroyed yet!";
			if(destroyedObjects.get(j).a == highlightedObject)
				highlightedObject = null;
			Object removed = null;
			search:
				for(int i = 0; i<worldObjects.size(); i++)
				{
					Placeable2D p = worldObjects.get(i).a;
					
					if(destroyedObjects.get(j).a == p)
					{
						if (selectedIndices.contains(new Integer(i)))
							selectedIndices.remove(new Integer(i)); //Deselect
						removed = worldObjects.remove(i); //Remove
						//Indices above have decreased, need to shuffle
						LinkedHashSet<Integer> old = new LinkedHashSet<Integer>(selectedIndices);
						selectedIndices.clear();
						for(Integer selIndex : old)
						{
							if(selIndex > i)
								selectedIndices.add(selIndex-1);
							else
								selectedIndices.add(selIndex);
						}
						indices[j] = i;
						break search; //Continue to next object
					}
				}
			assert( removed != null) : "Destroyed non-existing object: "+destroyedObjects.get(j).a;
		}
		return indices;
	}
	
	/**Called from an Operation when the indicated objects have already been revived.
	 * @param indices The indices where the objects should be reinserted into worldObjects.
	 * Note that this and the list of objects are traversed backwards, inserting the objects in that order.
	 * @param linkDatas The links to restore after reviving the objects*/
	public void objectsRevived(ArrayList<Pair<Placeable2D, ConstructorParams>> objects, int[] indices/*, LinkData[] linkDatas*/)
	{
		//Hootadoot, we need to add these objects at their previous positions.
		assert indices.length == objects.size();
		for(int i = indices.length-1; i>=0; i--)
		{
			assert !objects.get(i).a.isDestroyed() : objects.get(i).a+" hasn't been revived yet!";
			worldObjects.add(indices[i], objects.get(i));
		}
		//for(LinkData l : linkDatas)//TODO
		//    l.apply(this);
		//Now, StateInfo will be able select these objects correctly.
	}
	
	/**Returns the element in the linkedHashSet with the given index in the linked set.*/
	static <T> T extract(LinkedHashSet<T> linkedHashSet, int insertionIndex)
	{
		int count = 0;
		for(T t : linkedHashSet)
		{
			if(count == insertionIndex)
				return t;
			count++;
		}
		return null;
	}
	
	/**
	 * Returns the currently highlighted object. The highlighted object is the one that is under the mouse cursor.
	 * This is purely a visual property. Null is returned if there is no highlighted object.
	 */
	public Placeable2D getHighlightedObject()
	{
		return highlightedObject;
	}
	
	/**Return whether the indicated object is currently selected.
	 * 
	 * A selected object is rendered as blinking(as handled by Scene).
	 * The object is recognized if it is a Placeable or a Triplet holding a Placeable in field a*/
	public boolean isSelected(Object t)
	{
		int index = indexOf(t);
		if (index >= 0)
		{
			return selectedIndices.contains(index);
		}
		return false;
	}
	
	public int indexOf(Object t)
	{
		final Placeable2D p;
		if(t instanceof Placeable2D)
			p = (Placeable2D) t;
		else if(t instanceof Pair && ((Pair<?,?>)t).a instanceof Placeable2D)
			p = (Placeable2D)((Pair<?,?>)t).a;
		else	
			return -1;
		
		Integer cached = indexCache.get(p);
		if(cached != null && cached < worldObjects.size() && worldObjects.get(cached).a == p)
			return cached;
		
		int index = worldObjects.indexOf(t);
		if(index >= 0)
			return index;
		else
		{
			for(int i = 0; i<worldObjects.size(); i++)
			{
				if(worldObjects.get(i).a.equals(t))
				{
					indexCache.put(p, i);
					return i;
				}
			}
		}
		return -1;
	}
	
	public String getUniqueName(Placeable2D src, String defaultName)
	{
		defaultName = defaultName.replaceAll("[^A-Za-z0-9_]+", "");//Cull all letters that cannot be in a java identifier
		while(defaultName.length() > 0 && !Character.isJavaIdentifierStart(defaultName.charAt(0)))
			defaultName = defaultName.substring(1);//Prune letters from start that cannot be the start of a java identifier
		if(defaultName.length() == 0)
			defaultName = "object";//If nothing is left after sanitizing, default to "object"
		
		if(!isUniqueName(src, defaultName))
		{
			//Add a numerical constant after the name if the name was not unique
			if(defaultName.length()>=5 && defaultName.charAt(defaultName.length()-5) == '_')
				defaultName = defaultName.substring(0, defaultName.length()-5);
			DecimalFormat f = new DecimalFormat("0000");
			int cnt = 1;
			while(!isUniqueName(src, defaultName+'_'+f.format(cnt)))
				cnt++;
			defaultName = defaultName+'_'+f.format(cnt);
		}
		return defaultName;
	}
	/**Returns whether the given string is an acceptable name for a Placeable2D object.
	 * Note that this ignores the current world state. If an unique name is required, 
	 * use {@link #isUniqueName(Placeable2D, String)} or {@link #getUniqueName(Placeable2D, String)} instead.*/
	public static boolean isValidName(String name)
	{
		if(name == null || name.isEmpty())
			return false;
		if (!Character.isJavaIdentifierStart(name.charAt(0)))
			return false;
		for(int i = 1; i<name.length(); i++)
		{
			if (!Character.isJavaIdentifierPart(name.charAt(i)))
				return false;
		}
		return true;
	}
	
	public boolean isUniqueName(Placeable2D src, String name)
	{
		ArrayList<Pair<Placeable2D, ConstructorParams>> ps = worldObjects;
		for(int i = 0; i<ps.size(); i++)
		{
			Pair<Placeable2D, ConstructorParams> p = ps.get(i);
			if(p.a == src)
				continue;
			if(p.a.getName() != null && p.a.getName().equals(name))
				return false;
		}
		return true;
	}
	
	public ConstructorParams getSelectedConstructorParams() {
		if(editorUI == null)
			return null;
		else
			return editorUI.getSelectedConstructorParams();
	}
	/**May be called to terminate any current user operation.
	 * If cancel is true, then the mode shall, to the best of its ability, reset the world state to how it was before it started.
	 * However, this may not always be possible, in which case the mode must still terminate normally.*/
	public void terminateMode(boolean cancel)
	{
		if(mode != null)
			mode.terminate(cancel);
		assert mode == null : "Mode was terminated but never finished: "+mode;
	}
	/**Called from the mode object when it has completed its operations.*/
	public void modeFinished() 
	{
		mode = null;
		modeListeners.fireEvents(0);
		editorUI.setParamsLocked(false);
	}
	
	void save(File f) throws IOException
	{
		if(f == null)
			throw new FileNotFoundException("Null is not a file.");
		Output output = null;
		try 
		{
			backup(f);
			FileOutputStream fos = new FileOutputStream(f);
			output = new Output(fos);
			ArrayList<Operation> ops = new ArrayList<>();
			for(Pair<Placeable2D, ConstructorParams> p : worldObjects)
				ops.add(new ConstructorInvoker(p.b, new V2D(p.a.getX(), p.a.getY()), false, false));
			for (int i = 0; i < worldObjects.size(); i++)
			{
				Pair<Placeable2D, ConstructorParams> p = worldObjects.get(i);
				ops.add(new Transformation.TransformationData(i, p.a, null, null));
			}
			ops.add(new StateInfo(selectedIndices));
			
			Operation[] operations = new Operation[ops.size()];
			ops.toArray(operations);
			KryoInstance.kryo.writeClassAndObject(output, operations);
			
			currentFile = f;
			dumpDefaultMapName();
			settings.save(this);
		} catch(Exception e)
		{
			throw new IOException(e);
		}
		finally
		{
			if(output != null) 
				output.close();
		}
	}
	
	
	private void backup(File f) throws IOException
	{
		File[] backups = new File[NUM_BACKUPS+1];
		File backupsDir = new File(workingDir, "editorBackups");
		if((!backupsDir.exists() && !backupsDir.mkdirs()) || !backupsDir.isDirectory())
			throw new IOException("Could not create backups folder: "+backupsDir);
		
		backups[0] = f;
		for(int i = 1; i<NUM_BACKUPS+1; i++)
		{
			if(backups[i-1].exists())
				backups[i] = new File(backupsDir, f.getName()+".bak"+i);
			else
				break;
		}
		if(backups[NUM_BACKUPS] != null && backups[NUM_BACKUPS].exists())
			if (!backups[NUM_BACKUPS].delete())
				System.err.println("There was a problem deleting oldest backup: "+backups[NUM_BACKUPS]);
		for(int i = NUM_BACKUPS; i>0; i--)
		{
			if(backups[i] != null)
			{
				if (!backups[i-1].renameTo(backups[i]))
					System.err.println("There was a problem renaming backup: "+backups[i-1]+" to: "+backups[i]);
			}
		}
	}
	
	void open(File f) throws IOException
	{
		if(f == null)
			throw new FileNotFoundException("Null is not a file.");
		if(!f.exists())
			throw new FileNotFoundException(f+" doesn't exist.");
		
		if (f.getParent().endsWith("editorBackups"))
			JOptionPane.showMessageDialog(null, "Warning: The current map file is in a backups folder.\n"
				+ "You should save as a different file as soon as possible.", "Map is a backup", JOptionPane.WARNING_MESSAGE);
		
		ArrayList<Pair<Placeable2D, ConstructorParams>> worldObjectsOld = new ArrayList<>(worldObjects);
		LinkedHashSet<Integer> oldSelected = new LinkedHashSet<>(selectedIndices);
		
		Input in = null;
		try 
		{
			//Clear selection, it is for old instance set
			setSelectedIndices(null, false);
			//TODO append
			worldObjects.clear();
			
			in = new Input(new FileInputStream(f));
			Operation[] operations = (Operation[])KryoInstance.kryo.readClassAndObject(in);
			for(Operation o : operations)
			{
				o.apply(this);
			}
			
			//Delete old objects
			for(Pair<Placeable2D, ConstructorParams> ps : worldObjectsOld)
			{
				//Must make a best-effort to destroy all objects, this is past the point of no return
				try 
				{
					if(ps.a.isDestroyed())
						System.err.println("Warning: "+ps.a+" was prematurely destroyed when loading the new map");
					else
						ps.a.destroy();
				}
				catch(Exception e)
				{
					e.printStackTrace();
				}
			}
			
			undoList.clear();
			if(editorUI != null)
				editorUI.selectionChanged();
			currentFile = f;
			dumpDefaultMapName();
			settings.save(this);
		}
		catch(Exception e)
		{
			//Destroy new instances
			for(Pair<Placeable2D, ConstructorParams> ps : worldObjects)
				ps.a.destroy();
			
			//Restore old selection
			setSelectedIndices(oldSelected, false);
			
			//Restore old instance list
			worldObjects.clear();
			worldObjects.addAll(worldObjectsOld);
			//Restore old MapData //TODO?
			
			if(editorUI != null)
				editorUI.selectionChanged();
			throw new IOException(e);
		}
		finally
		{
			if(in != null) 
				in.close();
		}
		
	}
	
	void clearMap()
	{
		//Clear selection
		setSelectedIndices(null, false);
		//Delete all objects
		for(Pair<Placeable2D, ConstructorParams> ps : worldObjects)
		{
			//Must make a best-effort to destroy all objects, this is past the point of no return
			try 
			{
				if(ps.a.isDestroyed())
					System.err.println("Warning: "+ps.a+" was prematurely destroyed when loading the new map");
				else
					ps.a.destroy();
			}
			catch(Exception e)
			{
				e.printStackTrace();
			}
		}
		worldObjects.clear();
		//Nuke undo
		undoList.clear();
		//Notify UI
		if(editorUI != null)
			editorUI.selectionChanged();
		//No file
		currentFile = null;
	}
	
	void dumpDefaultMapName()
	{
		PrintWriter pw = null;
		try
		{
			Stack<String> reversePath = new Stack<>();
			File target = workingDir.getCanonicalFile();
			File search = currentFile.getCanonicalFile();
			boolean first = true;
			while(search != null)
			{
				//Delimiter
				if(!first)
					reversePath.push("/");
				//If current is the path we want to be relative to, then end
				if(search.equals(target))
					break;
				System.out.println(search+" is not equal to "+target+" : "+target.equals(search));
				reversePath.push(search.getName());//Current file/dir's name
				search = search.getParentFile();
				first = false;
			}
			if(search == null)
			{
				//We never found the working dir...
				System.err.println("Warning: "+currentFile+" is not relative to the working directory "+workingDir+", cannot use it as default map.");
				return;
			}
			StringBuilder sb = new StringBuilder();
			while(!reversePath.isEmpty())
			{
				sb.append(reversePath.pop());
			}
			String name = sb.toString();
			
			System.out.println("Setting default map: "+name);
			File f = new File(workingDir, "editorSettings/defaultMap.txt");
			pw = new PrintWriter(f);
			pw.println(name);
		}
		catch (Exception e)
		{
			System.err.println("Error: Could not save default map");
			e.printStackTrace();
		}
		finally
		{
			try{if(pw!=null)pw.close();}
			catch(Exception e){}
		}
	}
	File getDefaultMapName()
	{
		BufferedReader br = null;
		String ret = null;
		try
		{
			File f = new File(workingDir, "editorSettings/defaultMap.txt");
			br = new BufferedReader(new FileReader(f));
			ret = br.readLine();
			System.out.println("Default map is: "+ret);
		}
		catch (Exception e)
		{
			System.out.println("Could not read default map, using: "+ret);
			System.out.println('('+e.getMessage()+')');
		}
		finally
		{
			try{if(br!=null)br.close();}
			catch(Exception e){}
		}
		if(ret == null)
			return null;
		else
			return new File(workingDir, ret);
	}
	/**Returns the editor frame, which may be null if editing has never happened during this session.*/
	public StatefulJFrame getFrame()
	{
		if(editorUI == null)
			return null;
		return editorUI.editorFrame;
	}
	
	public Mode mode()
	{
		return mode;
	}
	
	public Editor2DUI editorUI()
	{
		return editorUI;
	}
	/***/
	public V2D getPivotPos()
	{
		V2D pivot = new V2D();
		if(selectedIndices.isEmpty())
			return null;
		Pivot pivotMode = settings.pivot.get();
		if(pivotMode == Pivot.FIRST_SELECTED)
		{
			Placeable2D myPivot = worldObjects.get(extract(selectedIndices, 0)).a;
			pivot.set(myPivot.getX(), myPivot.getY());
		}
		else if(pivotMode == Pivot.LAST_SELECTED)
		{
			Placeable2D myPivot = worldObjects.get(extract(selectedIndices, selectedIndices.size()-1)).a;
			pivot.set(myPivot.getX(), myPivot.getY());
		}
		else if(pivotMode == Pivot.MEAN)
		{
			pivot.set(V2D.ZERO);
			for(Integer selIndex : selectedIndices)
			{
				Placeable2D myPivot = worldObjects.get(selIndex).a;
				pivot.add(myPivot.getX(), myPivot.getY());
			}
			pivot.multiply(1d/selectedIndices.size());
		}
		else if(pivotMode == Pivot.MEDIAN)
		{
			double minX = Double.MAX_VALUE;
			double minY = Double.MAX_VALUE;
			double maxX = -Double.MAX_VALUE;
			double maxY = -Double.MAX_VALUE;
			for(Integer selIndex : selectedIndices)
			{
				Placeable2D myPivot = worldObjects.get(selIndex).a;
				if(myPivot.getX() < minX)
					minX = myPivot.getX();
				if(myPivot.getX() > maxX)
					maxX = myPivot.getX();
				if(myPivot.getY() < minY)
					minY = myPivot.getY();
				if(myPivot.getY() > maxY)
					maxY = myPivot.getY();
			}
			pivot.set((minX+maxX)/2, (minY+maxY)/2);
		}
		return pivot;
	}
	/**
	 * Add a game start listener. 
	 * There is no mechanism to automatically remove the listener, 
	 * so if the listener should be disposed before the editor then it should take care to remove itself explicitly.
	 * <br/><br/>
	 * <i>Note: Objects that are placed in the editor should <b>not</b> be registered using this mechanism. 
	 * They should instead implement the {@link Startable2D} interface unless there is a particular reason why this will not work.</i>
	 * <br/><br/>
	 * The game start event will be fired <i>after</i> {@link Startable2D#start()} has been called for all objects in the editor. 
	 */
	public void addGameStartListener(GameStartListener l)
	{
		startListeners.add(l);
	}
	/**
	 * Remove the indicated game start listener.
	 * @return Whether the listener was found in the list.
	 */
	public boolean removeGameStartListener(GameStartListener l)
	{
		return startListeners.remove(l);
	}
	
	public TTFFontRenderer getFont()
	{
		if(font == null)
			font = new TTFFontRenderer(new Font(Font.MONOSPACED, Font.PLAIN, 10), ColorF.WHITE);
		return font;
	}

	public void putLink(double x0, double y0, double x1, double y1, byte func, float[] c)
	{
		final int ARROW_SIZE = 6;
		V2D src = new V2D(x0, y0);
		V2D dst = new V2D(x1, y1);
		V2D along = new V2D(x1, y1).add(-x0, -y0).normalize();
		V2D perp = along.clone().perpendicular();
		
		if((func & Link.MASK_INPUT) != 0)
		{
			lineBuffer.put(src, c);
			lineBuffer.put(src.clone().add(perp, ARROW_SIZE).add(along, ARROW_SIZE), c);
			lineBuffer.put(src, c);
			lineBuffer.put(src.clone().add(perp, -ARROW_SIZE).add(along, ARROW_SIZE), c);
		}
		
		lineBuffer.put(src, c);
		lineBuffer.put(dst, c);
		
		if((func & Link.MASK_OUTPUT) != 0)
		{
			lineBuffer.put(dst, c);
			lineBuffer.put(dst.clone().add(perp, ARROW_SIZE).add(along, -ARROW_SIZE), c);
			lineBuffer.put(dst, c);
			lineBuffer.put(dst.clone().add(perp, -ARROW_SIZE).add(along, -ARROW_SIZE), c);
		}
	}
}
